package webserver;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.sql.Timestamp;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;

/**
 * Class containing all necessary informations regarding a HTTP/1.1 Request
 * 
 */
// <editor-fold defaultstate="collapsed" desc=" UML Marker "> 
// #[regen=yes,id=DCE.1204D4D4-90A5-4D03-22CA-38314A3BA62B]
// </editor-fold> 
public class HTTP11Request extends HTTP10Request {

	// Static Declarations
	private static SimpleDateFormat FORMATS[] = null;
	private static final String AVAILABLE_EXPECTATIONS = "100-continue";
	private static final String HEADER_CONTINUE = "HTTP/1.1 100 Continue\r\n\r\n";
	private static final String HEADER_NOTIMPLEMENTED = "HTTP/1.1 501 Not Implemented\r\n\r\n";
	private static final String HEADER_FILENOTFOUND = "HTTP/1.1 404 Not Found\r\n\r\n";
	private static final String HEADER_SUCCESSFUL = "HTTP/1.1 200 OK\r\n";
	private static final String HEADER_EXPECTATION_FAILED = "HTTP/1.1 417 Expectation Failed: ##\r\n\r\n";
	private static final String HEADER_BAD_REQUEST = "HTTP/1.1 400 Bad Request\r\n\r\n";
	private static final String HEADER_NOT_MODIFIED = "HTTP/1.1 304 Not Modified\r\n\r\n";
	private static final String HEADER_PRECONDITION_FAILED = "HTTP/1.1 412 Precondition Failed\r\n\r\n";
	// Variables
	// <editor-fold defaultstate="collapsed" desc=" UML Marker ">
	// #[regen=yes,id=DCE.4817E274-E18B-609F-E5CE-61F15981508E]
	// </editor-fold>
	private String hostHeader;
	// <editor-fold defaultstate="collapsed" desc=" UML Marker ">
	// #[regen=yes,id=DCE.6F18495D-0E2D-B3CB-6925-1005AD6B7841]
	// </editor-fold>

	/**
	 * Enum representing the two header fields If-Modified-Since (MODIFIED) or If-Unmodified-Since (UNMODIFIED) otherwise NONE
	 */
	public enum modifiedStateEnum {

		NONE, MODIFIED, UNMODIFIED
	};
	private modifiedStateEnum modifiedState;
	// <editor-fold defaultstate="collapsed" desc=" UML Marker ">
	// #[regen=yes,id=DCE.69D3A026-F2E5-727D-4C90-661BEBC10D14]
	// </editor-fold>
	private Timestamp modifiedTimestamp;

	/**
	 * HTTP11Request Default Constructor
	 * @param input - The input
	 */
	// <editor-fold defaultstate="collapsed" desc=" UML Marker ">
	// #[regen=yes,id=DCE.620549FA-C100-BF72-774E-F8B29194DEFD]
	// </editor-fold>
	public HTTP11Request(ArrayList<String> input) {
		super(input);

		this.keepalive = true;

		// Initialize Formats to recognize Timestamps
		if (FORMATS == null || FORMATS.length != 3) {
			FORMATS = new SimpleDateFormat[3];
			FORMATS[0] = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
			FORMATS[1] = new SimpleDateFormat("EEEE, dd-MMM-yy HH:mm:ss z", Locale.US);
			FORMATS[2] = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy", Locale.US);
		}
	}

	/**
	 * Parse input and get needed data
	 */
	@Override
	public void process() throws IllegalStateException {
		super.process();

		try {
			// Check Host Header
			if (!this.requestHeader.containsKey("Host")) {
				throw new IllegalStateException("No Host Header can be found in HTTP/1.1 Header!");
			} else {
				String[] temp = this.requestHeader.get("Host").split(":");
				this.requestHeader.remove("Host");
				this.requestHeader.put("Host", temp[0].trim());
				this.hostHeader = temp[0].trim();
			}

			// Check if Keep-Alive is set
			if (this.requestHeader.containsKey("Connection") && this.requestHeader.get("Connection").equals("Close")) {
				this.keepalive = false;
			}

			// Check if all expectations can be met
			if (this.requestHeader.containsKey("Expect")) {
				String[] expectations = this.requestHeader.get("Expect").split(";");

				for (int i = 0; i < expectations.length; i++) {
					if (!AVAILABLE_EXPECTATIONS.contains(expectations[i])) {
						String head = HEADER_EXPECTATION_FAILED;
						head = head.replace("##", expectations[i]);
						this.statuscode = 417;
						this.header = new byte[head.length()];
						this.header = head.getBytes();
						this.data = Cache.getInstance().getErrorPage(417);
						return;
					}
				}
			}

			// Check If-Modified-Since and If-Unmodified-Since
			this.modifiedState = modifiedStateEnum.NONE;
			this.modifiedTimestamp = null;
			String timestamp = null;
			// Check If-Modified-Since
			if (this.requestHeader.containsKey("If-Modified-Since")) {
				this.modifiedState = modifiedStateEnum.MODIFIED;
				timestamp = this.requestHeader.get("If-Modified-Since");
			}
			// Check If-Unmodified-Since
			if (this.requestHeader.containsKey("If-Unmodified-Since")) {
				if (this.modifiedState == modifiedStateEnum.MODIFIED) {
					throw new IllegalStateException("If-Unmodified-Since and If-Modified-Since Headers found!");
				}
				this.modifiedState = modifiedStateEnum.UNMODIFIED;
				timestamp = this.requestHeader.get("If-Unmodified-Since");
			}
			// Generate Modified-Timestamps if necessary
			if (this.modifiedState != modifiedStateEnum.NONE) {
				int i = 0;
				do {
					try {
						Date temp = FORMATS[i].parse(timestamp);
						this.modifiedTimestamp = new Timestamp(temp.getTime());
					} catch (ParseException pe) {
						i++;
					}
				} while (this.modifiedTimestamp == null && i < 3);

				// Invalid Timestamp
				if (new Timestamp(new java.util.Date().getTime()).before(modifiedTimestamp)) {
					Logger.writeToLog("Invalid Timestamp found in Header: " + modifiedTimestamp.toString());
					this.statuscode = 400;
					this.header = new byte[HEADER_BAD_REQUEST.length()];
					this.header = HEADER_BAD_REQUEST.getBytes();
					this.data = Cache.getInstance().getErrorPage(400);
				}

				if (this.modifiedTimestamp == null) {
					throw new IllegalStateException("Could not find Timestamp for: " + modifiedState.toString());
				}
			}

			// If the Header is supported and 100 continue is wanted, sent out
			if (this.requestType != requestTypeEnum.NOTIMPLEMENTED && this.requestHeader.containsKey("Expect") && this.requestHeader.get("Expect").contains("100-continue")) {
				this.send100Continue();

				try {
					// Read <content-length> bytes from the Input Stream
					InputStream read = this.socket.getInputStream();

					int datasize = Integer.parseInt(this.requestHeader.get("Content-Length"));

					StringBuffer tempData = new StringBuffer();
					for (int i = 0; i < datasize; i++) {
						tempData.append(Character.toChars(read.read()));
					}
					this.requestData.add(tempData.toString());
				} catch (IOException io) {
					Logger.writeToLog("IO Error reading data after 100-continue: " + io.toString());
				}
			}
		} catch (IllegalStateException ise) {
			Logger.writeToLog("process(): IllegalStateException " + ise.toString());
			this.statuscode = 400;
			this.header = new byte[HEADER_BAD_REQUEST.length()];
			this.header = HEADER_BAD_REQUEST.getBytes();
			this.data = Cache.getInstance().getErrorPage(400);
		}
	}

	/**
	 * Get the Host field in Header
	 * @return String
	 */
	// <editor-fold defaultstate="collapsed" desc=" UML Marker ">
	// #[regen=yes,regenBody=yes,id=DCE.AB1B315C-EAC6-7B99-15D5-35EBF17F7D95]
	// </editor-fold>
	public String getHostHeader() {
		return hostHeader;
	}

	/**
	 * Get the modified flags from header
	 * There are three possibilities: None, If-Modified-Since, If-Unmodified-Since
	 * @return modifiedStateEnum
	 */
	// <editor-fold defaultstate="collapsed" desc=" UML Marker ">
	// #[regen=yes,regenBody=yes,id=DCE.942D249D-B3C3-659A-69D8-B419D985331C]
	// </editor-fold>
	public modifiedStateEnum getModifiedState() {
		return modifiedState;
	}

	/**
	 * Get the Timestamp requested by If-Modified-Since or If-Unmodified-Since
	 * If not set, returns null
	 * @return Timestamp - can be null
	 */
	// <editor-fold defaultstate="collapsed" desc=" UML Marker ">
	// #[regen=yes,regenBody=yes,id=DCE.0E88A911-B381-75C0-63E8-BFFBBD12AD62]
	// </editor-fold>
	public Timestamp getModifiedTimestamp() {
		return modifiedTimestamp;
	}

	/**
	 * Get the absolut Path to the requested Ressource
	 * @return absolute path to the requested Ressource
	 */
	// <editor-fold defaultstate="collapsed" desc=" UML Marker ">
	// #[regen=yes,id=DCE.DCC8AFA0-4FBD-985C-40F1-B500002EFDA8]
	// </editor-fold>
	@Override
	public String getAbsPath() {
		return AppProperties.getInstance().getHomeDirectory() + "/" + this.hostHeader + "/" + this.relPath;
	}

	/**
	 * Send a "100 Continue"-Header
	 */
	public void send100Continue() {
		try {
			// Sent the "100 continue" Header
			PrintWriter printer = new PrintWriter(this.socket.getOutputStream());
			printer.write(HEADER_CONTINUE);
			printer.flush();
		} catch (IOException io) {
			Logger.writeToLog("Failed to send 100 continue for Request: " + this.toString() + ": " + io.toString());
		}
	}

	/**
	 * Set the state if regularly KeepAlives should be sent
	 * @param state
	 */
	public void setAutoKeepAlive(boolean state) {
		//if (!state) {
		//    this.keepAliveTimer.cancel();
		//} else {
		//    this.keepAliveTimer.scheduleAtFixedRate(this.keepAliveRequest, AppProperties.getInstance().getKeepAliveWaitInterval(), AppProperties.getInstance().getKeepAliveWaitInterval());
		//}
	}

	/**
	 * Generate the Response Header and Data
	 * @throws IllegalStateException
	 */
	@Override
	public void generateResponse() throws IllegalStateException {
		this.setAutoKeepAlive(false); // Stop sending KeepAlives as the Response will be processed now

		// Header has already been set by another function or something
		if (this.header != null) {
			return;
		}

		// Not Implemented
		if (this.requestType == requestTypeEnum.NOTIMPLEMENTED) {
			this.statuscode = 501;
			this.header = new byte[HEADER_NOTIMPLEMENTED.length()];
			this.header = HEADER_NOTIMPLEMENTED.getBytes();
			this.data = Cache.getInstance().getErrorPage(501);
			return;
		}

		// Get Request
		if (this.requestType == requestTypeEnum.GET) {
			String[] split = this.hostHeader.split(":");
			String path = split[0] + "/" + this.relPath;
			byte[] dataT = Cache.getInstance().getFileContents(path, this.recache);
			CacheItem item = Cache.getInstance().getCacheItem(path);

			// File not found
			if (dataT == null || dataT.length == 0) {
				this.statuscode = 404;
				this.header = new byte[HEADER_FILENOTFOUND.length()];
				this.header = HEADER_FILENOTFOUND.getBytes();
				this.data = Cache.getInstance().getErrorPage(404);
				return;
			}

			// Check If-Modified-Since Header
			if (this.modifiedState == modifiedStateEnum.MODIFIED) {
				if (item.lastModified().before(this.modifiedTimestamp)) {
					this.statuscode = 304;
					this.header = new byte[HEADER_NOT_MODIFIED.length()];
					this.header = HEADER_NOT_MODIFIED.getBytes();
					this.data = Cache.getInstance().getErrorPage(304);
					return;
				}
				// Check If-Unmodified-Since Header
			} else if (this.modifiedState == modifiedStateEnum.UNMODIFIED) {
				if (item.lastModified().after(this.modifiedTimestamp)) {
					this.statuscode = 412;
					this.header = new byte[HEADER_PRECONDITION_FAILED.length()];
					this.header = HEADER_PRECONDITION_FAILED.getBytes();
					this.data = Cache.getInstance().getErrorPage(412);
					return;
				}
			}

			// 200 - Successful
			this.statuscode = 200;
			String additionalHeader = this.generateHeaderItems(item);
			this.header = new byte[HEADER_SUCCESSFUL.length() + additionalHeader.length()];
			this.header = (HEADER_SUCCESSFUL + additionalHeader).getBytes();
			this.data = dataT;
			return;
		}

		// Post Request
		if (this.requestType == requestTypeEnum.POST) {
			File test = new File(AppProperties.getInstance().getHomeDirectory() + "\\" + this.hostHeader + "\\" + this.relPath);

			// 404 - File not Found
			if (!test.exists()) {
				this.statuscode = 404;
				this.header = new byte[HEADER_FILENOTFOUND.length()];
				this.header = HEADER_FILENOTFOUND.getBytes();
				this.data = Cache.getInstance().getErrorPage(404);
				return;
			}

			// 200 - Successful
			this.statuscode = 200;
			String additionalHeader = this.generateHeaderItems(null);
			this.header = new byte[HEADER_SUCCESSFUL.length() + additionalHeader.length()];
			this.header = (HEADER_SUCCESSFUL + additionalHeader).getBytes();
			String tData = this.generateRequestPage();
			this.data = new byte[tData.length()];
			this.data = tData.getBytes();
			return;
		}

		// Head Request
		if (this.requestType == requestTypeEnum.HEAD) {
			/*byte[] dataT = Cache.getInstance().getFileContents(this.relPath, this.recache);
			CacheItem item = Cache.getInstance().getCacheItem(this.relPath);*/
			String[] split = this.hostHeader.split(":");
			String path = split[0] + "/" + this.relPath;
			byte[] dataT = Cache.getInstance().getFileContents(path, this.recache);
			CacheItem item = Cache.getInstance().getCacheItem(path);

			// File not found
			if (dataT == null) {
				this.statuscode = 404;
				this.header = new byte[HEADER_FILENOTFOUND.length()];
				this.header = HEADER_FILENOTFOUND.getBytes();
				this.data = Cache.getInstance().getErrorPage(404);
				return;
			}

			// 200 - Successful
			this.statuscode = 200;
			String additionalHeader = this.generateHeaderItems(item);
			this.header = new byte[HEADER_SUCCESSFUL.length() + additionalHeader.length()];
			this.header = (HEADER_SUCCESSFUL + additionalHeader).getBytes();
			return;
		}

	}

	/**
	 * Generate additional Header Items for Response
	 * @param item - CacheItem
	 * @return String
	 */
	@Override
	protected String generateHeaderItems(CacheItem item) {
		StringBuffer fullHeader = new StringBuffer();

		// Get additional HTTP/1.0 Headers
		fullHeader.append(super.generateHeaderItems(item));

		return fullHeader.toString();
	}

	/**
	 * Get the absolut HTTP path to a requested file
	 * @return String
	 */
	@Override
	protected String getAbsHttpPath() {
		return "http://" + this.hostHeader + this.relPath;
	}

	/**
	 * Generate a Page representing the given POST informations
	 * @return String
	 */
	protected String generateRequestPage() {
		String tData = this.requestData.toString();

		// Head section
		String ret = "<html>\n<head>\n";
		ret = ret.concat("<title>POST Request</title>\n"); // Title
		ret = ret.concat("<style type=\"text/css\"><!--\n"); // Stylesheet
		ret = ret.concat("dt { font-weight:bold }\n");
		ret = ret.concat("--></style>\n");
		ret = ret.concat("</head>\n<body>\n");

		// Body section
		ret = ret.concat("<h2>Data contained in POST Request:</h2>\n<br />\n"); // H2
		ret = ret.concat("<dl>\n"); // Definition List

		// Parse data to fit in DL
		tData = tData.replace("&", "</dd>\n<dt>");
		tData = tData.replace("=", "</dt>\n<dd>");
		tData = tData.replace("[", "<dt>");
		tData = tData.replace("]", "</dd>\n");

		ret = ret.concat(tData);
		ret = ret.concat("</dl>\n</body>\n</html>"); // End of Document
		return ret;
	}

	/**
	 * Generate logfile entry for Combined Logfile Format
	 * @return String
	 */
	@Override
	public String getLogFileEntry() {
		StringBuffer ret = new StringBuffer(this.socket.getInetAddress().toString()); // Host
		ret.append("\t-"); // User
		ret.append("\t-"); // Password
		ret.append("\t" + FORMAT_LOGFILE.format(new java.util.Date().getTime())); // Timestamp
		ret.append("\t\"" + this.requestType.toString() + " " + this.relPath + " HTTP/1.1\""); // Request header
		ret.append("\t" + this.statuscode); // Statuscode
		//ret.append("\t" + this.data.length); // Byte count
		if (this.requestHeader.containsKey("Referer")) {
			ret.append("\t\"" + this.requestHeader.get("Referer") + "\""); // Referer
		} else {
			ret.append("\t-");
		}
		if (this.requestHeader.containsKey("User-Agent")) {
			ret.append("\t" + this.requestHeader.get("User-Agent")); // User-Agent
		} else {
			ret.append("\t-");
		}

		return ret.toString();
	}
}

